package persistence;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

import javassist.util.proxy.MethodFilter;
import javassist.util.proxy.ProxyFactory;
import javassist.util.proxy.ProxyObject;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import persistence.annotation.ManyToOne;
import persistence.annotation.OneToMany;
import persistence.annotation.Transient;

public class PersistentClass {

	private static Logger log = LoggerFactory.getLogger(PersistentClass.class);

	/**
	 * Le nom complet de la classe JAVA
	 */
	private String name;

	/**
	 * La classe JAVA
	 */
	private Class<?> clazz;

	/**
	 * Le nom de la table
	 */
	private String tableName;

	/**
	 * La liste des attributs persistants
	 */
	private List<PersistentProperty> persistentProperties = new ArrayList<PersistentProperty>();

	/**
	 * La propriété qui sert de clé primaire
	 */
	private PersistentProperty id;

	/**
	 * Tampon utilisé pour contruire les requêtes (statement)
	 */
	private StringBuffer sb = new StringBuffer();

	/**
	 * Indique si la classe analysée doit être proxy
	 */
	private boolean proxied;

	/**
	 * Indique si le proxy de la classe déjà été créé
	 */
	private boolean alreadyProxied = false;

	/**
	 * @throws PersistenceException
	 */
	public void parse() throws PersistenceException {

		try {
			clazz = Class.forName(getName());
		} catch (ClassNotFoundException e) {
			log.error("Mapped class not found:" + getName(), e);
			throw new PersistenceException("Mapped class not found: " + getName());
		}
		setTableName(clazz.getSimpleName().toLowerCase()); // par defaut

		// Iterer sur les méthodes get pour déterminer les attributs persistants
		Method[] methods = clazz.getMethods();
		for (int i = 0; i < methods.length; i++) {

			Method method = methods[i];
			String methodName = method.getName();
			PersistentProperty property = null;

			// Détection méthode quelconque (get, sans paramètre, non héritée
			// et
			// non Transient
			if (methodName.startsWith("get") && method.getParameterTypes().length == 0 && method.getDeclaringClass().equals(clazz) && method.getAnnotation(Transient.class) == null) {
				property = new PersistentProperty(this, method);
			}

			// Détection ManyToOne
			if (method.isAnnotationPresent(ManyToOne.class)) {
				this.proxied = true;
				property = new ManyToOneProperty(this, method);
			}

			// Détection OneToMany
			if (method.isAnnotationPresent(OneToMany.class)) {
				this.proxied = true;
				// property = new PersistentProperty(this, method);
			}

			// On ajoute la propriété si besoin
			if (property != null) {
				addPersistentProperty(property);
			}
		}
	}

	private void addPersistentProperty(PersistentProperty property) {
		if (!this.persistentProperties.contains(property))
			this.persistentProperties.add(property);
	}

	/**
	 * @return
	 * @throws PersistenceException
	 * @throws InstantiationException
	 * @throws IllegalAccessException
	 */
	public Object createInstance() throws PersistenceException, InstantiationException, IllegalAccessException {
		if (this.isProxied() && this.alreadyProxied == false) {
			this.alreadyProxied = true;
			this.createProxy();
		}
		Object created = clazz.newInstance();
		if (this.isProxied()) {
			((ProxyObject) created).setHandler(new PersistentClassHandler());
		}
		return created;
	}

	/**
	 * Création d'un proxy
	 * 
	 * @return
	 */
	public void createProxy() {

		ProxyFactory f = new ProxyFactory();
		f.setSuperclass(this.clazz);
		f.setFilter(new MethodFilter() {
			public boolean isHandled(Method m) {
				if (PersistentClass.this.toFilter(m)) {
					return true;
				}
				return false;
			}
		});
		clazz = f.createClass();
		log.info("Création proxy, class [" + this.clazz.getName() + "]");
	}

	/**
	 * Filtre les méthodes qui ont une annotion ManyToOne ou OneToMany
	 * 
	 * @param method
	 * @return true si la méthode contient une anntation ManyToOne ou OneToMany
	 * @return false sinon
	 */
	private boolean toFilter(Method method) {
		return method.isAnnotationPresent(ManyToOne.class) || method.isAnnotationPresent(OneToMany.class);
	}

	/**
	 * @return requête pour créer la table courante.
	 */
	public String getCreateTableStatement() {
		sb.setLength(0);
		sb.append("CREATE TABLE ");
		sb.append(this.tableName);
		sb.append("(");

		for (PersistentProperty pp : this.getPersistentProperties()) {
			sb.append(pp.getColumnName());
			sb.append(" ");
			sb.append(pp.getSqlType());
			sb.append(",");
		}

		// suppression derniere virgule
		sb = this.deleteVirgule(sb);
		sb.append(")");

		return sb.toString();
	}

	/**
	 * Supprime la dernière virgule d'une chaine de caractères
	 * 
	 * @param sb
	 * @return
	 */
	private StringBuffer deleteVirgule(StringBuffer sb) {
		return sb = new StringBuffer(sb.substring(0, sb.toString().length() - 1));
	}

	/**
	 * @return requête SQL pour supprimer la table courante.
	 */
	public String getDropTableStatement() {
		sb.setLength(0);
		sb.append("DROP TABLE ");
		sb.append(this.tableName);
		return sb.toString();
	}

	/**
	 * @return un prepared statement avec des <i>?</i> pour les valeurs colonnes
	 */
	public String getInsertStatement() {
		sb.setLength(0);
		sb.append("INSERT INTO ");
		sb.append(this.tableName);
		sb.append("(");
		// Ajout des colonnes
		for (PersistentProperty pp : this.getPersistentProperties()) {
			if (pp.isId()) {
				continue;
			}
			sb.append(pp.getColumnName());
			sb.append(",");
		}
		sb = this.deleteVirgule(sb);
		sb.append(") VALUES (");
		// Ajout des ?
		for (int i = 0; i < this.getPersistentProperties().size() - 1; i++) {
			sb.append("?,");
		}
		sb = this.deleteVirgule(sb);
		sb.append(")");
		return sb.toString();
	}

	/**
	 * @return un statement avec des ? pour les valeurs des colonnes et un ?
	 *         pour l'Id
	 */
	public String getUpdateStatement() {
		sb.setLength(0);
		sb.append("UPDATE ");
		sb.append(this.tableName);
		sb.append(" SET ");
		for (PersistentProperty pp : this.getPersistentProperties()) {
			if (pp.isId()) {
				continue;
			}
			sb.append(pp.getColumnName());
			sb.append("=?,");
		}
		sb = this.deleteVirgule(sb);
		sb.append(" WHERE id = ?");
		return sb.toString();
	}

	/**
	 * 
	 * @return un statement avec un ? Pour l'Id
	 */
	public String getSelectByIdStatement() {
		sb.setLength(0);
		sb.append("SELECT * FROM ");
		sb.append(this.tableName);
		sb.append(" WHERE id = ? ");
		return sb.toString();
	}

	/**
	 * @param persistentProperty
	 * @return
	 */
	public String getSelectByPropertyStatement(PersistentProperty persistentProperty) {
		sb.setLength(0);
		sb.append("SELECT * FROM ");
		sb.append(this.tableName);
		sb.append(" WHERE ");
		sb.append(persistentProperty.getColumnName());
		sb.append(" =  ?");
		return sb.toString();
	}

	/**
	 * @return
	 */
	public String getSelectAllStatement() {
		sb.setLength(0);
		sb.append("SELECT * FROM ");
		sb.append(this.tableName);
		return sb.toString();
	}

	/**
	 * @return
	 */
	public String getDeleteStatement() {
		sb.setLength(0);
		sb.append("DELETE FROM ");
		sb.append(this.tableName);
		sb.append("WHERE id = ?");
		return sb.toString();
	}

	/**
	 * 
	 * @param name
	 * @return
	 * @throws PersistenceException
	 */
	public PersistentProperty getPersistentProperty(String name) throws PersistenceException {
		for (PersistentProperty persistentProperty : this.persistentProperties) {
			String ppName = persistentProperty.getMethod().getName();
			if (ppName.equalsIgnoreCase(name) || ppName.substring(3, ppName.length()).equalsIgnoreCase(name)) {
				return persistentProperty;
			}
		}
		throw new PersistenceException("Impossible de recuperer la propriete : " + name);
	}

	public boolean isProxied() {
		return proxied;
	}

	/**
	 * @return the persistentProperties
	 */
	public List<PersistentProperty> getPersistentProperties() {
		return persistentProperties;
	}

	/**
	 * @param persistentProperties
	 *            the persistentProperties to set
	 */
	public void setPersistentProperties(List<PersistentProperty> persistentProperties) {
		this.persistentProperties = persistentProperties;
	}

	/**
	 * @return the name
	 */
	public String getName() {
		return name;
	}

	/**
	 * @param name
	 *            the name to set
	 */
	public void setName(String name) {
		this.name = name;
	}

	/**
	 * @return the clazz
	 */
	public Class<?> getClazz() {
		return clazz;
	}

	/**
	 * @param clazz
	 *            the clazz to set
	 */
	public void setClazz(Class<?> clazz) {
		this.clazz = clazz;
	}

	/**
	 * @return the tableName
	 */
	public String getTableName() {
		return tableName;
	}

	/**
	 * @param tableName
	 *            the tableName to set
	 */
	public void setTableName(String tableName) {
		this.tableName = tableName;
	}

	/**
	 * @return the id
	 */
	public PersistentProperty getId() {
		return id;
	}

	/**
	 * @param id
	 *            the id to set
	 */
	public void setId(PersistentProperty id) {
		this.id = id;
	}

}
